unit CPPlotCanvas;

interface

uses Classes, Controls, Graphics, CPBase;

type
  { This class provides painting methods for drawing on a control
    similar to TCanvas. TCPPlotCanvas itself is an abstract class,
    it only defines the interface of the painting methods. Descendants of
    TCPPlotCanvas implement the actual painting operations. }
  TCPPlotCanvas = class
  protected
    { Parent control }
    FControl: TCustomControl;
    { Canvas of parent control }
    FCanvas: TCanvas;
    { Canvas bounds }
    FBounds: TCPBounds;
    { Sepcifies transformation mode of cooridnate units to canvas units.
      This has to be true for canvas implementations that use canvas
      coordinates with the origin in the top left corner (e.g. Windows GDI).
      For implementations with the origin being in the bottom right corner
      (e.g. OpenGL) this has to be set to false. }
    FMirrorYAxis: Boolean;
    { Current pen color }
    FPenColor: TColor;
    { Current brush color }
    FBrushColor: TColor;
    { Current drawing position in canvas units }
    FDrawPos: TCPVector;
    { True, between calls of BeginDraw and EndDraw }
    FDrawing: Boolean;
    { Background color of control }
    FColor: TColor;
    { Font used for text rendering }
    FFont: TFont;
    { Font height in canvas units }
    FFontHeight: TCPFloat;
    { Canvas units per inch ratio. This value describes how the canvas units
      relates to a "real" length unit. This value is used to translate font
      size and line thickness (both specified in pt) to canvas units.
      @seealso Font
      @seealso FPenWidth }
    FUnitsPerInch: TCPFloat;
    { Line thickness in pt (1 pt = 1/72 inch) }
    FPenWidth: TCPFloat;
    { Line thickness in discrete canvas units. This value is used for
      implementations with no sub-pixel accuracy. }
    FPenDiscreteUnitWidth: Integer;
    { True, if the canvas uses discrete units. If the canvas supports sub pixel
      accuracy, this flag is false. }
    FUseDiscreteUnits: Boolean;
    { Sets new canvas bounds }
    procedure SetBounds(const Value: TCPBounds); virtual;
    { Sets new background color }
    procedure SetColor(const Value: TColor); virtual;
    { Sets the font that is used for text rendering }
    procedure SetFont(const Value: TFont);
    { Sets the brush color }
    procedure SetBrushColor(const Value: TColor); virtual;
    { Sets the pen color }
    procedure SetPenColor(const Value: TColor); virtual;
    { Sets the line width in pt }
    procedure SetPenWidth(const Value: TCPFloat); virtual;
    { Called when the canvas bounds are changed. }
    procedure Resize; virtuaL;
    { Called, when the font is changed. }
    procedure FontChanged; virtual;
    { Sets a new ratio of a canvas unit to an inch }
    procedure SetUnitsPerInch(const Value: TCPFloat); virtual;
  public
    { Create the canvas on the referenced parent control.
      @param Control Plotter control) }
    constructor Create(Control: TCustomControl); overload; virtual;
    { Create the canvas on the referenced parent control.
      @param Control Plotter control)
      @param UnitsPerInch Canvas units per inch ratio }
    constructor Create(Control: TCustomControl;
      UnitsPerInch: TCPFloat); overload; virtual;
    destructor Destroy; override;
    { Smooths distances for screen canvas implementations with no sub pixel
      accuracy. Drawing certain graphical elements (e.g. axis marks) on a
      canvas with no sub pixel accuracy can lead to pixel-wobbling outlines.
      In these cases certain lengths, such as the length of a line, need to be
      rounded to an integer value, which is performed by this method. For
      canvas implementation with enabled sub-pixel accuracy this method returns
      the distance without modification. }
    function SmoothDistance(Distance: TCPFloat): TCPFloat; virtual;
    { Returns the width and height of some text in canvas units as it would
      be drawn on the canvas in the current font. }
    function TextExtent(const Text: string): TCPVector; virtual;
    { Returns the width of some text in canvas units when it would
      be drawn on the canvas in the current font. }
    function TextWidth(Text: string): TCPFloat;
    { Returns the height of some text in canvas units when it would
      be drawn on the canvas in the current font. }
    function TextHeight(Text: string): TCPFloat;
    { Moves the drawing cursor to the specified position. }
    procedure MoveTo(X, Y: TCPFloat); virtual;
    { Draws a line from the position of the drawing cursor
      to the specified position. Floating point version }
    procedure LineTo(X, Y: TCPFloat); virtual;
    { Draws a horizontal line along the specified Y position. }
    procedure HorizLine(Y: TCPFloat); virtual; abstract;
    { Draws a vertical line along the specified X position. }
    procedure VertLine(X: TCPFloat); virtual; abstract;
    { Draws a number of line segments. Every i-th line segment is drawn from
      (Left + i * StepSize, Points[i]) to
      (Left + (i + 1) * StepSize, Points[i + 1]), where i is the index of the
      line segment. }
    procedure DrawLineSegments(Points: TCPFloatArray; Left, Right: TCPFloat;
      StepSize: Double); virtual;
    { Draws an ellipse at the specified position. }
    procedure Ellipse(const Bounds: TCPBounds); virtual; abstract;
    { Draws a rectangle at the specified position filled with the current
      brush color and outlined with the current pen color. }
    procedure Rectangle(const Bounds: TCPBounds); virtual; abstract;
    { Draws a rectangle at the specified position outlined with the current
      pen color. }
    procedure FrameRect(const Bounds: TCPBounds); virtual; abstract;
    { Draws a rectangle at the specified position filled with the current
      brush color. }
    procedure FillRect(const Bounds: TCPBounds); virtual; abstract;
    { Draws text at the specified position. }
    procedure TextOut(X, Y: TCPFloat; const Text: string); virtual; abstract;
    { Prepares the canvas for performing drawing operations. }
    procedure BeginDraw; virtual;
    { Notifies the canvas that drawing operations are completed
     (for example to swap buffers for a canvas class using double buffering). }
    procedure EndDraw; virtual;
    { Converts a value in pt to canvas units. }
    function PtToUnits(const Value: TCPFloat): TCPFloat;
    { Converts a value in canvas units to pt. }
    function UnitsToPt(const Value: TCPFloat): TCPFloat;
    { Current pen color. }
    property PenColor: TColor read FPenColor write SetPenColor;
    { Current brush color. }
    property BrushColor: TColor read FBrushColor write SetBrushColor;
    { Drawing flag, true between calls of BeginDraw and EndDraw. }
    property Drawing: Boolean read FDrawing;
    { Background color of the canvas. }
    property Color: TColor read FColor write SetColor;
    { Font used for text rendering }
    property Font: TFont read FFont write SetFont;
    { Canvas dimensions in canvas units }
    property Bounds: TCPBounds read FBounds write SetBounds;
    { Line thickness in pt }
    property PenWidth: TCPFloat read FPenWidth write SetPenWidth;
    { Mirrors Y-axis when trnasforming coordinates to canvas units }
    property MirrorYAxis: Boolean read FMirrorYAxis;
    { True, if the canvas uses discrete units. If the canvas supports sub pixel
      accuracy, this flag is false. }
    property UseDiscreteUnits: Boolean read FUseDiscreteUnits;
    { Sets or gets the ratio of a canvas unit to an inch. }
    property UnitsPerInch: TCPFloat read FUnitsPerInch write SetUnitsPerInch;
  end;

  TCPPlotCanvasClass = class of TCPPlotCanvas;

implementation

uses Types, CPPlotter, Windows, Math;

{ TCPPlotCanvas }

procedure TCPPlotCanvas.BeginDraw;
begin
  FDrawing := true;
end;

constructor TCPPlotCanvas.Create(Control: TCustomControl);
var
  DPI: Integer;
  Plotter: TCPPlotter;
begin
  Plotter := Control as TCPPlotter;
  DPI := GetDeviceCaps(Plotter.Canvas.Handle, LOGPIXELSX);
  Create(Control, DPI * Plotter.DisplayZoom);
end;

constructor TCPPlotCanvas.Create(Control: TCustomControl;
  UnitsPerInch: TCPFloat);
begin
  FControl := Control;
  FCanvas := (FControl as TCPPlotter).Canvas;
  FDrawPos := CPToVector(0, 0);
  FBounds := CPToBounds(0, 0, 1, 1);
  FPenWidth := 0.0;
  FPenDiscreteUnitWidth := 0;
  FFont := TFont.Create;
  FUnitsPerInch := UnitsPerInch;
  FMirrorYAxis := true;
  FUseDiscreteUnits := false;
end;


procedure TCPPlotCanvas.EndDraw;
begin
  FDrawing := false;
end;

procedure TCPPlotCanvas.LineTo(X, Y: TCPFloat);
begin
  MoveTo(X, Y);
end;

procedure TCPPlotCanvas.MoveTo(X, Y: TCPFloat);
begin
  FDrawPos := CPToVector(X, Y);
end;

function TCPPlotCanvas.PtToUnits(const Value: TCPFloat): TCPFloat;
begin
  Result := Value * FUnitsPerInch / 72;
end;

function TCPPlotCanvas.UnitsToPt(const Value: TCPFloat): TCPFloat;
begin
  Result := Value * 72 / FUnitsPerInch;
end;

procedure TCPPlotCanvas.SetBounds(const Value: TCPBounds);
begin
  FBounds := Value;
  // Avoid floating point exceptions:
  if Abs(FBounds.Width) < 1E-5 then
    FBounds.Width := 1;
  if Abs(FBounds.Height) < 1E-5 then
    FBounds.Height := 1;
  Resize;
end;

procedure TCPPlotCanvas.SetBrushColor(const Value: TColor);
begin
  FBrushColor := ColorToRGB(Value);
end;

procedure TCPPlotCanvas.SetColor(const Value: TColor);
begin
  FColor := ColorToRGB(Value);
end;

procedure TCPPlotCanvas.SetFont(const Value: TFont);
begin
  if (Value <> FFont) then
  begin
    FFont.Assign(Value);
    FontChanged;
  end;
end;

procedure TCPPlotCanvas.SetPenColor(const Value: TColor);
begin
  FPenColor := ColorToRGB(Value);
end;

procedure TCPPlotCanvas.SetPenWidth(const Value: TCPFloat);
begin
  FPenWidth := Value;
  FPenDiscreteUnitWidth := Max(1, Round(PtToUnits(FPenWidth)));
end;

function TCPPlotCanvas.TextExtent(const Text: string): TCPVector;
var
  Size: TSize;
begin
  Size := FCanvas.TextExtent(Text);
  Result := CPToVector(Size.cx, Size.cy);
end;

function TCPPlotCanvas.TextHeight(Text: string): TCPFloat;
begin
  Result := TextExtent(Text).Y;
end;

function TCPPlotCanvas.TextWidth(Text: string): TCPFloat;
begin
  Result := TextExtent(Text).X;
end;

procedure TCPPlotCanvas.Resize;
begin
  { Reacts on canvas sizes } 
end;

function TCPPlotCanvas.SmoothDistance(Distance: TCPFloat): TCPFloat;
begin
  Result := Distance;
end;

procedure TCPPlotCanvas.DrawLineSegments(Points: TCPFloatArray; Left,
  Right: TCPFloat; StepSize: Double);
var
  Pt: TCPPoint;
  Index, Count: Integer;
  DrawingLine: Boolean;
begin
  Index := 0;
  DrawingLine := false;
  Pt.X := Left;
  Count := Length(Points);
  while (Index < Count) do
  begin
    Pt.Y := Points[Index];
    if (IsNan(Pt.Y) = false) and
       (IsInfinite(Pt.Y) = false) then
    begin
      if DrawingLine then
        LineTo(Pt.X, Pt.Y)
      else
      begin
        MoveTo(Pt.X, Pt.Y);
        DrawingLine := true;
      end;
    end
    else
      // Interrupt line, due to undefined value in definition range:
      DrawingLine := false;
    Inc(Index);
    // Move to next graph point:
    Pt.X := Pt.X + StepSize;
  end;
end;

destructor TCPPlotCanvas.Destroy;
begin
  FFont.Free;
  inherited;
end;

procedure TCPPlotCanvas.FontChanged;
begin
  FFontHeight := PtToUnits(FFont.Size);
  // Also assign font to canvas, so that TextExtent gives correct results:
  FCanvas.Font.Assign(FFont);
end;

procedure TCPPlotCanvas.SetUnitsPerInch(const Value: TCPFloat);
begin
  FUnitsPerInch := Value;
  PenWidth := FPenWidth;
  FontChanged;
end;

end.
